<?php

/**
 * Контроллер и контейнер компонентов многостраничной формы.
 *
 */
abstract class Kernel_Form_Wizard extends Kernel_Controller_Tokened implements Kernel_Form_Storage {

    /**
     * Наименование переменной сессии, в которой хранятся данные формы.
     *
     */
    const VARNAME = 'wizard';

    /**
     * Наименования шагов формы в той последовательности, в которой они должны
     * выполняться и сквозные параметры, которые должны передаваться в эти шаги.
     *
     * @var array
     */
    protected $_steps = array();

    /**
     * Описание ключа записей, редактируемых в форме.
     *
     * @var array
     */
    protected $_key = array();

    /**
     * Сохраняет ли фильтр сохраняет параметры для вложенных фильтров.
     *
     * @var boolean
     */
    protected $_through = true;

    /**
     * Путь к файлу шаблона страницы с сообщением о том, что форма сохранена.
     * Если не указан, форма просто будет перерисована.
     *
     * @var string
     */
    protected $_template_saved = 'blank.tpl';

    /**
     * Наименование текущего шага формы.
     *
     * @var string
     */
    private $_step;

    /**
     * Идентификатор текущей сессии многостраничной формы.
     *
     * @var string
     */
    private $_session;

    /**
     * Конструктор.
     *
     */
    public function __construct($param = array()) {
        parent::__construct($param);

        if ($this->_sessionValid($session = Application::getRequest()->get('session'))) {
            $this->_session = $session;
        } else {
            $this->_sessionStart();
        }
    }

    /**
     * Загружает данные формы.
     *
     * @return  array
     */
    public function load() {
        return $this->_loadStep($this->_step);
    }

    /**
     * Возвращает сохраненные на указанном шаге данные формы.
     *
     * @param   string  $step
     * @return  array
     */
    public function get($step = null) {
        return null !== $step
            ? ($this->_passed($step) ? $this->_get($step) : array())
            : $_SESSION[self::VARNAME][$this->_session]['data'];
    }

    /**
     * Сохраняет данные формы.
     *
     * @param   array   $values
     */
    public function save($values) {
        $this->_saveStep($this->_step, $values);
    }

    /**
     * Временно. Вызывается после сохранения данных формы.
     *
     */
    public function done() {
        if (false !== ($next = $this->_next())) {
            $this->_redirect(array('step' => $next));
        } else {
            $this->_save();
            $this->_clear();
            Application::getResponse()->setTemplate($this->_template_saved)
                ->message(true, array(get_class($this), 'ADDED'));
        }
    }

    /**
     * Возвращает описание компонента, который обработает действие, полученное
     * из запроса.
     *
     * @return  mixed
     */
    protected function _getFilterToken() {

        // получаем наименование текущего шага
        if (!is_string($step = Application::getRequest()->get('step'))) {

            // выполняем внешнее перенаправление на шаг по умолчанию
            $this->_redirect(array(), true);
        }

        if (!array_key_exists($step, $this->_steps)) {
            $this->_abort('Step not found');
        }

        $allowed = $this->_isAllowed($step);

        switch (true) {
            case false === $allowed:
                $this->_deny('No one step is allowed');
                break;
            case is_string($allowed):
                $this->_redirect(array('step' => $allowed));
                break;
        }

        return $this->_token($this->_step = $step);
    }

    /**
     * Возвращает наименование класса компонента, который обработает запрошенное
     * действие.
     *
     * @param   string  $name
     * @return  string
     */
    protected function _getFilterClassName($name) {
        return get_class($this) . '_' . str_camelize($name);
    }

    /**
     * Возвращает параметры компонента-реализации указанного шага.
     *
     * @param   string  $step
     * @return  array
     */
    protected function _params($step) {
        return array_merge(
            array_intersect_key($this->_params, array_flip($this->_steps[$step])),
            array('storage' => $this)
        );
    }

    /**
     * Загружает данные формы на указанном шаге.
     *
     * @param   string  $step
     * @return  array
     */
    protected function _loadStep($step) {
        return $this->_passed($step) ? $this->_get($step) : $this->_fetch($step);
    }

    /**
     * Сохраняет данные формы на указанном шаге.
     *
     * @param   string  $step
     * @param   array   $values
     */
    protected function _saveStep($step, $values) {
        $_SESSION[self::VARNAME][$this->_session]['data'][$step] = $values;
    }

    /**
     * Получает исходные данные для формы на указанном шаге.
     *
     * @param   string  $step
     * @return  array
     */
    protected function _fetch($step) {
        // по умолчанию инициируем загрузку данных из самой формы
        return $this->_new() ? array() : $this->_step($step)->load();
    }

    /**
     * Сохраняет данные многостраничной формы.
     *
     */
    protected function _save() {
        // по умолчанию просто сохраняем данные каждой из форм
        foreach (array_keys($this->_steps) as $step) {
            $this->_step($step)->save($this->get($step));
        }
    }

    /**
     * Очищает данные сессии.
     *
     */
    protected function _clear() {
        unset($_SESSION[self::VARNAME][$this->_session]['data']);
    }

    /**
     * Возвращает значение поля формы на указанном шаге.
     *
     * @param   string  $step
     * @param   string  $name
     * @return  mixed
     */
    protected function _getValue($step, $name = null) {
        if ($this->_passed($step)) {
            $vars = $this->_get($step);
            return null === $name
                ? $vars
                : get_array_element($vars, $name);
        }
        return null;
    }

    /**
     * Возвращает, бил ли успешно пройден указанный шаг.
     *
     * @param   string  $step
     * @return  boolean
     */
    protected function _passed($step) {
        return isset($_SESSION[self::VARNAME][$this->_session]['data'][$step]);
    }

    /**
     * Возвращает наименование предыдущего шага.
     *
     * @return  string
     */
    protected function _previous() {
        return $this->_offset(-1);
    }

    /**
     * Возвращает наименование следующего шага.
     *
     * @return  string
     */
    protected function _next() {
        return $this->_offset(+1);
    }

    /**
     * Возвращает уникальный ключ редактируемой записи в оригинальном или
     * строковом представлении.
     *
     * @param   boolean $serialized
     * @return  array
     */
    private function _key($serialized = false) {
        $key = array_extract($this->_params, $this->_key, null);
        return $serialized ? md5(serialize($key)) : $key;
    }

    /**
     * Возвращает, создается ли в форме новая запись.
     *
     * @return  boolean
     */
    private function _new() {
        return !array_filter($this->_key());
    }

    /**
     * Возвращает токен, соответствующий указанному шагу.
     *
     * @param   string  $step
     * @return  Kernel_Token
     */
    private function _token($step) {
        return new Kernel_Token($step, $this->_params($step));
    }

    /**
     * Фозвращает форму, соответствующую указанному шагу.
     *
     * @param   string  $step
     * @return  Kernel_Action_Form
     */
    private function _step($step) {
        return $this->_construct($this->_token($step));
    }

    /**
     * Возвращает, разрешено ли пользователю находиться на шаге с указанным
     * наименованием. По умолчанию опрашиваются все компоненты до текущего
     * включительно.
     *
     * @param   string  $name
     * @return  boolean
     */
    private function _isAllowed($name) {
        foreach (array_keys($this->_steps) as $step) {
            if ($name == $step) {
                return true;
            }
            if (!$this->_passed($step)) {
                return $step;
            }
        }
        return false;
    }

    /**
     * Перенаправляет пользователя на указанный шаг/сессию.
     *
     * @param   array   $params
     * @param   boolean $permanent
     */
    private function _redirect($params, $permanent = false) {
        $this->_location($params + array(
            'session' => $this->_session,
            'step' => key($this->_steps),
        ), $permanent);
    }

    /**
     * Возвращает наименование следующего шага, находящегося на указанном
     * смещении от текущего.
     *
     * @return  string
     */
    private function _offset($offset) {
        $steps = array_keys($this->_steps);
        return false !== ($current = array_search($this->_step, $steps))
            && array_key_exists($key = $current + $offset, $steps) ? $steps[$key] : false;
    }

    /**
     * Возвращает данные указанного шага.
     *
     * @param   string  $step
     * @return  array
     */
    private function _get($step) {
        return $_SESSION[self::VARNAME][$this->_session]['data'][$step];
    }

    /**
     * Проверяет правильность указанного идентификатора сессии.
     *
     * @param   string  $session
     * @return  boolean
     */
    private function _sessionValid($session) {
        return isset($_SESSION[self::VARNAME][$session])
            && $this->_key() == $_SESSION[self::VARNAME][$session]['key'];
    }

    /**
     * Создает идентификатор сессии.
     *
     * @return  string
     */
    private function _sessionStart() {

        // генерируем идентификатор сессии
        $session = md5(uniqid(rand()));

        // заполняем данные сессии исходными значениями
        $_SESSION[self::VARNAME][$session] = array(
            'key' => $this->_key(),
            'data' => array(),
        );

        // перенаправляем клиента с явным указанием идентификатора сессии
        $this->_redirect(array('session' => $session));
    }
}
